package com.bungarus.busi;

import com.bungarus.Access;
import com.bungarus.codec.protobuf.MessageProtobuf;
import com.bungarus.event.AuthSucceededEvent;
import com.bungarus.model.Member;
import com.bungarus.model.MessageBuilder;
import com.bungarus.model.Session;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.concurrent.Future;
import org.apache.log4j.Logger;
import org.springframework.util.StringUtils;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 *
 * Created by tang on 2018/11/11.
 */
public class Authentication extends AccessBusiProcessing{
    private static Logger logger = Logger.getLogger(Authentication.class);

    private ChannelHandlerContext channelHandlerContext;
    private static final int AUTH_TIMEOUT = 1500;
    private static final int SESSION_EXPIRATION = 2 * 60 * 60 * 1000;

    //constructor
    public Authentication(ChannelHandlerContext channelHandlerContext) {
        this.channelHandlerContext = channelHandlerContext;
    }

    @Override
    public void doBusiness(Object o) throws BusiProcessingException {
        MessageProtobuf.Message message = (MessageProtobuf.Message)o;
        MessageProtobuf.Header header = message.getHeader();
        MessageProtobuf.Credential credential = message.getHeader().getCredential();
        String sessionID = header.getSessionId();

        //Basic auth has the highest priority
        if(!StringUtils.isEmpty(credential.getUsername()) && !StringUtils.isEmpty(credential.getPwd())) { //username and password are both provided, do basic auth
            login(() -> {
                Login localLogin = AccessBusiProcessing.springContext.getBean(LocalLogin.class);
                Member member = localLogin.login(credential.getUsername(), credential.getPwd());
                if(null != member && member.getId() > 0) {  //login succeeded
                    this.channelHandlerContext.fireUserEventTriggered(
                            new AuthSucceededEvent()
                                .setMember(member)
                                .setHeader(header)
                    );
                    return true;
                }
                else {
                    this.channelHandlerContext.fireExceptionCaught(new UserNonexistException());
                    return false;
                }
            });
        }
        else if(StringUtils.isEmpty(sessionID) && !StringUtils.isEmpty(header.getToken())){ //if no sessionID, then re-auth, can this type of message(no session) be received here?
            login(() -> {
                //TODO: remote re-auth according to the token
                this.channelHandlerContext.writeAndFlush(MessageBuilder.buildAuthSucceed("123421jj","dojolqnmdoge"));
                return true;
            });
        }
        else if(!StringUtils.isEmpty(sessionID)){
            login(() -> {
                Session session = new Session();
                session.setId(sessionID);
                Channel channel = Access.getSessionContainer().find(session);
                if(null != channel){
                    session = Access.getSessionContainer().findSession(sessionID);
                    if(System.currentTimeMillis() - session.getCreated() > SESSION_EXPIRATION) { // if session timeout
                        this.channelHandlerContext.fireExceptionCaught(new AuthenticationExpirationException());
                        return false;
                    }
                }
                else {
                    this.channelHandlerContext.fireExceptionCaught(new AuthenticationExpirationException());
                    return false;
                }
                this.channelHandlerContext.fireChannelRead(o);
                return true;
            });
        }
        else {
            this.channelHandlerContext.fireExceptionCaught(new AuthenticationInputException());
        }
    }

    private void login(Callable<Boolean> localOrRemote){
        Future<Boolean> result = BusiProcessing.authPool.submit(localOrRemote);
        try {
            result.get(AUTH_TIMEOUT, TimeUnit.MILLISECONDS);
        }
        catch (Exception e) {
            result.cancel(true);
            e.printStackTrace();
            channelHandlerContext.fireExceptionCaught(new BusiTimeoutException());
        }
    }
}
